package org.lds.hotkey.client.holder;

import io.etcd.jetcd.KeyValue;
import io.etcd.jetcd.Watch;
import io.etcd.jetcd.watch.WatchEvent;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.BooleanUtils;
import org.lds.hotkey.client.autoconfigure.component.AppContext;
import org.lds.hotkey.client.common.ClientErrorEnum;
import org.lds.hotkey.common.autoconfigure.etcd.EtcdService;
import org.lds.hotkey.common.autoconfigure.spring.SpringUtil;
import org.lds.hotkey.common.constant.KeyConstant;
import org.lds.hotkey.common.enums.ErrorEnum;
import org.lds.hotkey.common.model.Entity;
import org.lds.hotkey.common.model.RuleModel;
import org.lds.hotkey.common.util.AssertUtil;
import org.lds.hotkey.common.util.EntityModelUtil;
import org.lds.hotkey.common.util.JsonUtil;

import java.util.*;
import java.util.stream.Collectors;

@Slf4j
public class RuleHolder {
    private static final EtcdService etcdService = SpringUtil.getBean(EtcdService.class);

    private static final Set<RuleModel> RULE_INFO = new TreeSet<>(Comparator.comparingInt(RuleModel::getPriority));
    private static final Map<String, RuleModel> RULE_MAP = new HashMap<>();

    public static void start() {
        AssertUtil.assertTrue(Objects.nonNull(AppContext.appId), ClientErrorEnum.APP_ID_NOT_EXIST);

        init();
        watch();
    }

    /**
     * 初始化 RULE_MAP
     */
    public static void init() {
        List<KeyValue> keyValues = etcdService.listByPrefix(KeyConstant.RULE_DIRECTORY + AppContext.appId);
        AssertUtil.assertTrue(Objects.nonNull(keyValues), ErrorEnum.ETCD_UNAVAILABLE);

        for (KeyValue keyValue : keyValues) {
            String value = keyValue.getValue().toString();
            add(JsonUtil.fromJson(value, RuleModel.class));
        }

        log.info("RULE_INFO={} RULE_MAP={}", JsonUtil.toJson(RULE_INFO), JsonUtil.toJson(RULE_MAP));
    }

    public static void watch() {
        Watch.Watcher watcher = etcdService.watchKey(KeyConstant.RULE_DIRECTORY + AppContext.appId, watchResponse -> {
            for (WatchEvent event : watchResponse.getEvents()) {
                Entity<RuleModel> entity = EntityModelUtil.getEntity(event, RuleModel.class);
                if (Objects.isNull(entity)) continue;

                RULE_INFO.remove(RuleModel.builder().id(entity.getId()).build());
                if (event.getEventType() == WatchEvent.EventType.PUT) {
                    add(entity.getValue());
                } else {
                    RULE_MAP.remove(entity.getValue().getKey());
                }
            }
            log.info("RULE_INFO={} RULE_MAP={}", JsonUtil.toJson(RULE_INFO), JsonUtil.toJson(RULE_MAP));
        });
        AssertUtil.assertTrue(Objects.nonNull(watcher), ErrorEnum.ETCD_UNAVAILABLE);
    }

    /**
     * 优先级: 1. 精确匹配 2: 前缀匹配 3: 通配符匹配 *
     * @param key
     * @return
     */
    public static RuleModel getRuleModel(String key) {
        List<RuleModel> ruleModels = new ArrayList<>();
        ruleModels.addAll(RULE_INFO);

        // 1. 精确匹配
        RuleModel ruleModelEquals = matchEquals(key);
        if (Objects.nonNull(ruleModelEquals)) return ruleModelEquals;

        // 2: 前缀匹配
        RuleModel ruleModelPrefix = matchPrefix(ruleModels, key);
        if (Objects.nonNull(ruleModelPrefix)) return ruleModelPrefix;

        // 3. 通配符匹配 *
        return matchEquals("*");
    }

    /**
     * 匹配equals
     * @return
     */
    private static RuleModel matchEquals(String key) {
        return RULE_MAP.get(key);
    }

    /**
     * 匹配前缀
     * @param ruleModels
     * @return
     */
    private static RuleModel matchPrefix(List<RuleModel> ruleModels, String key) {
        List<RuleModel> prefixRuleModels = ruleModels.stream().filter(ruleModel -> BooleanUtils.isTrue(ruleModel.getPrefix())).collect(Collectors.toList());
        for (RuleModel ruleModel : prefixRuleModels) {
            if (key.startsWith(ruleModel.getKey())) return ruleModel;
        }
        return null;
    }

    private static void add(RuleModel ruleModel) {
        if (Objects.isNull(ruleModel)) return;

        // 只存储前缀的数据
        if (ruleModel.getPrefix()) {
            RULE_INFO.add(ruleModel);
        }

        // 只存储非前缀的数据
        else {
            RULE_MAP.put(ruleModel.getKey(), ruleModel);
        }
    }
}
